Làm chủ tệp khai báo TypeScript (.d.ts) để mở khóa an toàn kiểu và tự động hoàn thành cho mọi thư viện JavaScript. Học cách sử dụng @types, tạo định nghĩa của riêng bạn và xử lý mã nguồn bên thứ ba một cách chuyên nghiệp.
Mở Khóa Hệ Sinh Thái JavaScript: Tìm Hiểu Sâu về Tệp Khai Báo TypeScript
TypeScript đã cách mạng hóa lĩnh vực phát triển web hiện đại bằng cách mang lại kiểu tĩnh cho thế giới năng động của JavaScript. An toàn kiểu này mang lại những lợi ích đáng kinh ngạc: phát hiện lỗi ở thời điểm biên dịch, cho phép tự động hoàn thành mã mạnh mẽ trong trình soạn thảo, và làm cho các codebase lớn dễ bảo trì hơn đáng kể. Tuy nhiên, một thách thức lớn nảy sinh khi chúng ta muốn sử dụng hệ sinh thái rộng lớn của các thư viện JavaScript hiện có—hầu hết chúng không được viết bằng TypeScript. Làm thế nào để mã TypeScript được định kiểu chặt chẽ của chúng ta hiểu được hình dạng, hàm và biến từ một thư viện JavaScript không có kiểu?
Câu trả lời nằm ở Tệp Khai Báo TypeScript (TypeScript Declaration Files). Những tệp này, có thể nhận biết qua phần mở rộng .d.ts, là cầu nối thiết yếu giữa thế giới TypeScript và JavaScript. Chúng hoạt động như một bản thiết kế hoặc một hợp đồng API, mô tả các kiểu của một thư viện bên thứ ba mà không chứa bất kỳ mã thực thi nào của nó. Trong hướng dẫn toàn diện này, chúng ta sẽ khám phá mọi thứ bạn cần biết để tự tin quản lý các định nghĩa kiểu cho bất kỳ thư viện JavaScript nào trong các dự án TypeScript của bạn.
Tệp Khai Báo TypeScript Chính Xác Là Gì?
Hãy tưởng tượng bạn đã thuê một nhà thầu chỉ nói một ngôn ngữ khác. Để làm việc hiệu quả với họ, bạn sẽ cần một người phiên dịch hoặc một bộ hướng dẫn chi tiết bằng ngôn ngữ mà cả hai đều hiểu. Một tệp khai báo phục vụ chính xác mục đích này cho trình biên dịch TypeScript (nhà thầu).
Một tệp .d.ts chỉ chứa thông tin về kiểu. Nó bao gồm:
- Chữ ký cho các hàm và phương thức (kiểu tham số, kiểu trả về).
- Định nghĩa cho các biến và kiểu của chúng.
- Interface và bí danh kiểu (type alias) cho các đối tượng phức tạp.
- Định nghĩa lớp (class), bao gồm các thuộc tính và phương thức của chúng.
- Cấu trúc namespace và module.
Điều quan trọng là, các tệp này không chứa mã thực thi. Chúng hoàn toàn dành cho việc phân tích tĩnh. Khi bạn nhập một thư viện JavaScript như Lodash vào dự án TypeScript của mình, trình biên dịch sẽ tìm kiếm một tệp khai báo tương ứng. Nếu tìm thấy, nó có thể xác thực mã của bạn, cung cấp tự động hoàn thành thông minh và đảm bảo bạn đang sử dụng thư viện một cách chính xác. Nếu không, nó sẽ báo lỗi như: Could not find a declaration file for module 'lodash'.
Tại Sao Tệp Khai Báo Là Bắt Buộc Đối Với Lập Trình Chuyên Nghiệp
Sử dụng các thư viện JavaScript mà không có định nghĩa kiểu phù hợp trong một dự án TypeScript sẽ làm suy yếu chính lý do sử dụng TypeScript ngay từ đầu. Hãy xem xét một kịch bản đơn giản sử dụng thư viện tiện ích phổ biến, Lodash.
Thế Giới Không Có Định Nghĩa Kiểu
Nếu không có tệp khai báo, TypeScript không biết lodash là gì hoặc nó chứa những gì. Để mã có thể biên dịch được, bạn có thể bị cám dỗ sử dụng một giải pháp nhanh như sau:
const _: any = require('lodash');
const users = [{ 'user': 'barney' }, { 'user': 'fred' }];
// Tự động hoàn thành? Không có trợ giúp ở đây.
// Kiểm tra kiểu? Không. 'username' có phải là thuộc tính đúng không?
// Trình biên dịch cho phép điều này, nhưng nó có thể thất bại khi chạy.
_.find(users, { username: 'fred' });
Trong trường hợp này, biến _ có kiểu any. Điều này thực chất nói với TypeScript, "Đừng kiểm tra bất cứ thứ gì liên quan đến biến này." Bạn mất tất cả các lợi ích: không có tự động hoàn thành, không kiểm tra kiểu trên các đối số, và không chắc chắn về kiểu trả về. Đây là mảnh đất màu mỡ cho các lỗi runtime.
Thế Giới Với Định Nghĩa Kiểu
Bây giờ, hãy xem điều gì xảy ra khi chúng ta cung cấp tệp khai báo cần thiết. Sau khi cài đặt các kiểu (chúng ta sẽ đề cập đến phần tiếp theo), trải nghiệm được biến đổi hoàn toàn:
import _ from 'lodash';
interface User {
user: string;
active?: boolean;
}
const users: User[] = [{ 'user': 'barney' }, { 'user': 'fred' }];
// 1. Trình soạn thảo cung cấp tự động hoàn thành cho 'find' và các hàm lodash khác.
// 2. Di chuột qua 'find' sẽ hiển thị đầy đủ chữ ký và tài liệu của nó.
// 3. TypeScript thấy rằng `users` là một mảng các đối tượng `User`.
// 4. TypeScript biết vị từ cho `find` trên `User[]` nên liên quan đến `user` hoặc `active`.
// ĐÚNG: TypeScript không báo lỗi.
const fred = _.find(users, { user: 'fred' });
// LỖI: TypeScript phát hiện ra sai lầm!
// Thuộc tính 'username' không tồn tại trên kiểu 'User'.
const betty = _.find(users, { username: 'betty' });
Sự khác biệt là một trời một vực. Chúng ta có được sự an toàn kiểu hoàn toàn, trải nghiệm nhà phát triển vượt trội thông qua các công cụ, và giảm đáng kể các lỗi tiềm ẩn. Đây là tiêu chuẩn chuyên nghiệp để làm việc với TypeScript.
Hệ Thống Phân Cấp Tìm Kiếm Định Nghĩa Kiểu
Vậy, làm thế nào để bạn có được những tệp .d.ts kỳ diệu này cho các thư viện yêu thích của mình? Có một quy trình đã được thiết lập rõ ràng bao quát phần lớn các tình huống.
Bước 1: Kiểm tra xem Thư viện có Tích hợp Sẵn Kiểu của Chính nó không
Trường hợp tốt nhất là khi một thư viện được viết bằng TypeScript hoặc các nhà bảo trì của nó cung cấp các tệp khai báo chính thức trong cùng một gói. Điều này ngày càng trở nên phổ biến đối với các dự án hiện đại, được bảo trì tốt.
Cách kiểm tra:
- Cài đặt thư viện như bình thường:
npm install axios - Nhìn vào bên trong thư mục của thư viện trong
node_modules/axios. Bạn có thấy bất kỳ tệp.d.tsnào không? - Kiểm tra tệp
package.jsoncủa thư viện để tìm trường"types"hoặc"typings". Trường này trỏ trực tiếp đến tệp khai báo chính. Ví dụ,package.jsoncủa Axios chứa:"types": "index.d.ts".
Nếu các điều kiện này được đáp ứng, bạn đã hoàn tất! TypeScript sẽ tự động tìm và sử dụng các kiểu được tích hợp sẵn này. Không cần hành động gì thêm.
Bước 2: Dự án DefinitelyTyped (@types)
Đối với hàng ngàn thư viện JavaScript không tích hợp sẵn kiểu của chúng, cộng đồng TypeScript toàn cầu đã tạo ra một nguồn tài nguyên đáng kinh ngạc: DefinitelyTyped.
DefinitelyTyped là một kho lưu trữ tập trung, do cộng đồng quản lý trên GitHub, nơi lưu trữ các tệp khai báo chất lượng cao cho một số lượng lớn các gói JavaScript. Các định nghĩa này được xuất bản lên registry npm dưới phạm vi @types.
Cách sử dụng:
Nếu một thư viện như lodash không tích hợp sẵn kiểu của nó, bạn chỉ cần cài đặt gói @types tương ứng của nó như một phụ thuộc phát triển (development dependency):
npm install --save-dev @types/lodash
Quy ước đặt tên rất đơn giản và dễ đoán: đối với một gói có tên package-name, các kiểu của nó hầu như luôn ở @types/package-name. Bạn có thể tìm kiếm các kiểu có sẵn trên trang web npm hoặc trực tiếp trên kho lưu trữ DefinitelyTyped.
Tại sao lại là --save-dev? Các tệp khai báo chỉ cần thiết trong quá trình phát triển và biên dịch. Chúng không chứa bất kỳ mã runtime nào, vì vậy chúng không nên được bao gồm trong gói sản phẩm cuối cùng của bạn. Cài đặt chúng như một devDependency đảm bảo sự tách biệt này.
Bước 3: Khi Không có Kiểu nào Tồn tại - Tự Viết Kiểu của Riêng Bạn
Điều gì sẽ xảy ra nếu bạn đang sử dụng một thư viện cũ, ít phổ biến hoặc thư viện nội bộ riêng tư không tích hợp sẵn kiểu và không có trên DefinitelyTyped? Trong trường hợp này, bạn cần xắn tay áo lên và tạo tệp khai báo của riêng mình. Mặc dù điều này nghe có vẻ đáng sợ, bạn có thể bắt đầu đơn giản và thêm chi tiết hơn khi cần thiết.
Giải Pháp Nhanh: Khai Báo Module Môi Trường Dạng Viết Tắt
Đôi khi, bạn chỉ cần làm cho dự án của mình biên dịch mà không có lỗi trong khi bạn tìm ra một chiến lược định kiểu phù hợp. Bạn có thể tạo một tệp trong dự án của mình (ví dụ: declarations.d.ts hoặc types/global.d.ts) và thêm một khai báo viết tắt:
// trong một tệp .d.ts
declare module 'some-untyped-library';
Điều này nói với TypeScript, "Tin tôi đi, có một module tên là 'some-untyped-library' tồn tại. Cứ coi mọi thứ được nhập từ nó đều có kiểu any." Điều này làm im lặng lỗi của trình biên dịch, nhưng như chúng ta đã thảo luận, nó hy sinh tất cả sự an toàn kiểu cho thư viện đó. Đây là một bản vá tạm thời, không phải là một giải pháp lâu dài.
Tạo một Tệp Khai Báo Tùy Chỉnh Cơ Bản
Một cách tiếp cận tốt hơn là bắt đầu định nghĩa các kiểu cho các phần của thư viện mà bạn thực sự sử dụng. Giả sử chúng ta có một thư viện đơn giản tên là `string-utils` xuất ra một hàm duy nhất.
// Trong node_modules/string-utils/index.js
module.exports.capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
Chúng ta có thể tạo một tệp string-utils.d.ts trong một thư mục `types` riêng trong thư mục gốc của dự án.
// Trong my-project/types/string-utils.d.ts
declare module 'string-utils' {
export function capitalize(str: string): string;
// Bạn có thể thêm các định nghĩa hàm khác ở đây khi sử dụng chúng
// export function slugify(str: string): string;
}
Bây giờ, chúng ta cần cho TypeScript biết nơi tìm các định nghĩa kiểu tùy chỉnh của mình. Chúng ta làm điều này trong tsconfig.json:
{
"compilerOptions": {
// ... các tùy chọn khác
"baseUrl": ".",
"paths": {
"*": ["types/*"]
}
}
}
Với thiết lập này, khi bạn import { capitalize } from 'string-utils', TypeScript sẽ tìm thấy tệp khai báo tùy chỉnh của bạn và cung cấp sự an toàn kiểu mà bạn đã định nghĩa. Bạn có thể dần dần xây dựng tệp này khi sử dụng nhiều tính năng hơn của thư viện.
Tìm Hiểu Sâu Hơn: Viết Tệp Khai Báo
Hãy cùng khám phá một số khái niệm nâng cao hơn mà bạn sẽ gặp phải khi viết hoặc đọc các tệp khai báo.
Khai Báo Các Loại Export Khác Nhau
Các module JavaScript có thể xuất (export) mọi thứ theo nhiều cách khác nhau. Tệp khai báo của bạn phải khớp với cấu trúc export của thư viện.
- Named Exports (Export theo tên): Đây là loại phổ biến nhất. Chúng ta đã thấy nó ở trên với `export function capitalize(...)`. Bạn cũng có thể export hằng số, interface và class.
- Default Export (Export mặc định): Dành cho các thư viện sử dụng `export default`.
- UMD Globals: Đối với các thư viện cũ hơn được thiết kế để hoạt động trong trình duyệt thông qua thẻ
<script>, chúng thường tự gắn vào đối tượng `window` toàn cục. Bạn có thể khai báo các biến toàn cục này. - `export =` và `import = require()`: Cú pháp này dành cho các module CommonJS cũ hơn sử dụng `module.exports = ...`. Ví dụ, nếu một thư viện thực hiện `module.exports = myClass;`.
declare module 'my-lib' {
export const version: string;
export interface Options { retries: number; }
export function doSomething(options: Options): Promise
declare module 'my-default-lib' {
// Đối với một hàm export mặc định
export default function myCoolFunction(): void;
// Đối với một đối tượng export mặc định
// const myLib = { name: 'lib', version: '1.0' };
// export default myLib;
}
// Khai báo một biến toàn cục '$' của một kiểu nhất định
declare var $: JQueryStatic;
// trong my-class.d.ts
declare class MyClass { constructor(name: string); }
export = MyClass;
// trong app.ts của bạn
import MyClass = require('my-class');
const instance = new MyClass('test');
Mặc dù ít phổ biến hơn với các ES Module hiện đại, điều này rất quan trọng để tương thích với nhiều gói Node.js cũ nhưng vẫn được sử dụng rộng rãi.
Mở Rộng Module (Module Augmentation): Mở Rộng Các Kiểu Hiện Có
Một trong những tính năng mạnh mẽ nhất là mở rộng module (còn được gọi là hợp nhất khai báo - declaration merging). Điều này cho phép bạn thêm các thuộc tính vào các interface hiện có được định nghĩa trong tệp khai báo của một gói khác. Điều này cực kỳ hữu ích cho các thư viện có kiến trúc plugin, như Express hoặc Fastify.
Hãy tưởng tượng bạn đang sử dụng một middleware trong Express thêm thuộc tính `user` vào đối tượng `Request`. Nếu không có mở rộng, TypeScript sẽ phàn nàn rằng `user` không tồn tại trên `Request`.
Đây là cách bạn có thể cho TypeScript biết về thuộc tính mới này:
// trong tệp types/express.d.ts của bạn
// Chúng ta phải import kiểu gốc để mở rộng nó
import { UserProfile } from './auth'; // Giả sử bạn có một kiểu UserProfile
// Cho TypeScript biết chúng ta đang mở rộng module 'express-serve-static-core'
declare module 'express-serve-static-core' {
// Nhắm mục tiêu đến interface 'Request' bên trong module đó
interface Request {
// Thêm thuộc tính tùy chỉnh của chúng ta
user?: UserProfile;
}
}
Bây giờ, trong toàn bộ ứng dụng của bạn, đối tượng `Request` của Express sẽ được định kiểu chính xác với thuộc tính `user` tùy chọn, và bạn sẽ có được sự an toàn kiểu và tự động hoàn thành đầy đủ.
Chỉ Thị Ba Gạch Chéo (Triple-Slash Directives)
Đôi khi bạn có thể thấy các chú thích ở đầu tệp .d.ts bắt đầu bằng ba dấu gạch chéo (///). Đây là các chỉ thị ba gạch chéo, hoạt động như các hướng dẫn cho trình biên dịch.
/// <reference types="..." />: Đây là loại phổ biến nhất. Nó bao gồm một cách tường minh các định nghĩa kiểu của một gói khác như một phụ thuộc. Ví dụ, các kiểu cho một plugin WebdriverIO có thể bao gồm/// <reference types="webdriverio" />bởi vì các kiểu của chính nó phụ thuộc vào các kiểu cốt lõi của WebdriverIO./// <reference path="..." />: Điều này được sử dụng để khai báo một phụ thuộc vào một tệp khác trong cùng một dự án. Đây là một cú pháp cũ hơn, phần lớn đã được thay thế bằng import của ES module.
Các Phương Pháp Tốt Nhất để Quản lý Tệp Khai Báo
- Ưu tiên Kiểu Tích hợp Sẵn: Khi lựa chọn giữa các thư viện, hãy ưu tiên những thư viện được viết bằng TypeScript hoặc tích hợp sẵn các định nghĩa kiểu chính thức của chúng. Điều đó báo hiệu một cam kết với hệ sinh thái TypeScript.
- Giữ
@typestrongdevDependencies: Luôn cài đặt các gói@typesvới--save-devhoặc-D. Chúng không cần thiết cho mã sản phẩm của bạn. - Căn chỉnh Phiên bản: Một nguồn lỗi phổ biến là sự không khớp giữa phiên bản thư viện và phiên bản
@typescủa nó. Một bản nâng cấp phiên bản chính trong một thư viện (ví dụ: từ v2 lên v3) có thể sẽ có những thay đổi đột phá trong API của nó, điều này phải được phản ánh trong gói@types. Cố gắng giữ chúng đồng bộ. - Sử dụng
tsconfig.jsonđể Kiểm soát: Các tùy chọn trình biên dịchtypeRootsvàtypestrongtsconfig.jsoncủa bạn có thể cho bạn quyền kiểm soát chi tiết về nơi TypeScript tìm kiếm các tệp khai báo.typeRootscho trình biên dịch biết thư mục nào cần kiểm tra (mặc định là./node_modules/@types), vàtypescho phép bạn liệt kê rõ ràng các gói kiểu nào cần bao gồm. - Đóng góp Trở lại: Nếu bạn viết một tệp khai báo toàn diện cho một thư viện chưa có, hãy cân nhắc đóng góp nó cho dự án DefinitelyTyped. Đây là một cách tuyệt vời để đền đáp cho cộng đồng nhà phát triển toàn cầu và giúp đỡ hàng ngàn người khác.
Kết Luận: Những Người Hùng Thầm Lặng của An Toàn Kiểu
Tệp Khai Báo TypeScript là những người hùng thầm lặng giúp chúng ta có thể tích hợp liền mạch thế giới JavaScript năng động, rộng lớn vào một môi trường phát triển mạnh mẽ, an toàn về kiểu. Chúng là liên kết quan trọng giúp tăng cường sức mạnh cho các công cụ của chúng ta, ngăn chặn vô số lỗi và làm cho codebase của chúng ta trở nên kiên cường và tự ghi lại tài liệu hơn.
Bằng cách hiểu cách tìm, sử dụng và thậm chí tạo ra các tệp .d.ts của riêng mình, bạn không chỉ đang sửa một lỗi trình biên dịch—bạn đang nâng cao toàn bộ quy trình phát triển của mình. Bạn đang mở khóa toàn bộ tiềm năng của cả TypeScript và hệ sinh thái phong phú của các thư viện JavaScript, tạo ra một sức mạnh tổng hợp mang lại phần mềm tốt hơn, đáng tin cậy hơn cho khán giả toàn cầu.